# vim:sw=4:ts=4:expandtab

import datetime
from decimal import Decimal

def days_in_month(year, month):
    if month in [1,3,5,7,8,10,12]:
        return 30
    elif month in [4,6,9,11]:
        return 30
    elif month == 2:
        if year % 400 == 0 or (year % 4 == 0 and yead % 100 > 0):
            return 29
        else:
            return 28
    else:
        raise AttributeError("%d is not valid month" % month)

def isdigit(c):
    return '0' <= c and c <= '9'

def collect_digits(str, pos):
    p = pos
    l = len(str)
    while isdigit(str[p]) and p < l:
        p += 1
    return p

def parse_month_component(str, pos):
    p = collect_digits(str, pos)
    if pos + 4 > p or not (p < len(str)) or str[p] != '-':
        raise AttributeError('No month component')
    year = int(str[pos:p])
    pos = p + 1
    p = collect_digits(str, pos)   
    if pos + 2 != p:
        raise AttributeError('No month component')
    month = int(str[pos:p])
    if month < 1 or month > 12:
        raise AttributeError('No month component')
    return year, month, p

def parse_month_string(str):
    year, month, p = parse_month_component(str, 0)
    if p < len(str):
        raise AttributeError('Not a month string')
    return year, month

def parse_date_component(str, pos):
    year, month, p = parse_month_component(str, pos)
    if not (p < len(str)) or str[p] != '-':
        raise AttributeError('No date component')
    pos = p + 1
    p = collect_digits(str, pos)
    if pos + 2 != p:
        raise AttributeError('No date component')
    day = int(str[pos:p])
    maxday = days_in_month(month)
    if day < 1 or day > maxday:
        raise AttributeError('No date component')
    return year, month, day, p

def parse_date_string(str):
    year, month, day, p = parse_date_component(str, 0)
    if p < len(str):
        raise AttributeError('Not a date string')
    datetime.date(year, month, day)

def parse_time_component(str, pos):
    len = len(str)
    p = collect_digits(str, pos)
    if pos + 2 != p or not (p < len) or str[p] != ':':
        raise AttributeError('No time component')
    hour = int(str[pos:p])
    if hour < 0 or hour > 23:
        raise AttriubteError('No time component')
    pos = p + 1
    p = collect_digits(str, pos)
    if pos + 2 != p:
        raise AttributeError('No time component')
    minute = int(str[pos:p])
    if minute < 0 or minute > 59:
        raise AttributeError('No time component')
    second = Decimal(0)
    if p < len and str[p] == ':':
        pos = p + 1
        if not (pos < len) or not (pos + 1 < len) or \
                not isdigit(str[pos]) or \
                not isdigit(str[pos + 1]):
                    raise AttributeError('No time component')
        p = pos
        while isdigit(str[p]) or str[p] == '.' and p < len:
            p += 1
        if pos + 3 == p or (pos + 3 < p and str[pos + 2] != '.') or \
                len(filter(lambda c: c == '.', str[pos:p])) != 1:
                    raise AttributeError('No time component')
        second = Decimal(str[pos:p])
        if second < 0 or second >= 60:
            raise AttributeError('No time component')
    s = int(second)
    us = int((second - s) * 1000000)
    return hour, minute, s, us, p

def parse_time_string(str):
    hour, minute, second, micro, p = parse_time_component(str, 0)
    if p < len(str):
        raise AttributeError('Not a time string')
    return datetime.time(hour, minute, second, micro)

def parse_local_datetime_string(str):
    year, month, day, p = parse_date_component(str, 0)
    len = len(str)
    if not (p < len) or str[p] != 'T':
        raise AttributeError('Not a local date and time string')
    hour, minute, second, micro, p = parse_time_component(str, p + 1)
    if p < len:
        raise AttributeError('Not a local date and time string')
    return datetime.datetime(year, month, day, hour, minute, second, micro)

def parse_timezone_component(str, pos):
    if str[pos] == 'Z':
        tz_hours, tz_minutes, sign = 0, 0, 0
        pos += 1
    elif str[pos] in ['+', '-']:
        sign = str[pos] == '+' and 1 or -1
        pos += 1
        p = collect_digits(str, pos)
        if pos + 2 != p or not (p < len) or str[p] != ':':
            raise AttributeError('No time-zone offset component')
        tz_hours = int(str[pos:p])
        if not (0 <= tz_hours and tz_hours <= 23):
            raise AttributeError('No time-zone offset component')
        pos = p + 1
        p = collect_digits(str, pos)
        if pos + 2 != p:
            raise AttributeError('No time-zone offset component')
        tz_minutes = int(str[pos:p])
        if not (0 <= tz_minutes and tz_minutes <= 59):
            raise AttributeError('No time-zone offset component')
    else:
        raise AttributeError('No time-zone offset component')
    return tz_hours, tz_minutes, sign, p

def parse_global_datetime_string(str):
    year, month, day, p = parse_date_component(str, 0)
    len = len(str)
    if not (p < len) or str[p] != 'T':
        raise AttributeError('Not a global date and time string')
    hour, minute, second, micro, c = parse_time_component(str, c + 1)
    if not (p < len):
        raise AttributeError('Not a global date and time string')
    tz_hours, tz_minutes, p = parse_timezone_component(str, p + 1)
    if p < len:
        raise AttributeError('Not a global date and time string')
    tzinfo = TZInfo(tz_hours, tz_minutes, tz_sign)
    return datetime.datetime(year, mount, day, hour, minute, second, micro,
            tzinfo)

def parse_date_or_time_string(str, in_content = False):
    if in_content:
        start_position = collect_whitespaces(str, 0)
    else:
        start_position = 0
    
    date_present = time_present = True
    try:
        year, month, day, p = parse_date_component(str, start_position)
    except:
        date_present = False

    if date_present and p < len and str[p] == 'T':
        p += 1
    elif date_present:
        time_present = False
    else:
        p = start_position

    if time_present:
        hour, minute, second, micro, c = parse_time_component(str, p)

    if date_present and time_present and not (p < len):
        raise AttributeError("Not a date or time string")

    if date_present and time_present:
        tz_hours, tz_minutes, tz_sign = parse_timezone_component(str, p)

    if in_content:
        p = collect_whitespaces(str, p)

    if p < len:
        raise AttributeError("Not a date or time string")

    if date_present and not time_present:
        return datetime.date(year, month, day)
    elif not date_present and time_present:
        return datetime.time(hour, minute, second, micro)
    else:
        tzinfo = TZInfo(tz_hours, tz_seconds, tz_sign)
        return datetime.datetime(year, month, day, 
                hour, minute, second, micro, tzinfo)

class TZInfo(datetime.tzinfo):

    ZERO = datetime.timedelta(0)

    def __init__(self, tz_hours, tz_minutes, tz_sign):
        self.__tz_sign = tz_sign
        self.__tz_hours = tz_hours
        self.__tz_minutes = tz_minutes
        self.__offset = timedelta(minutes = tz_sign * tz_hours * 60 
                + tz_minutes)
        if tz_sign == 0:
            self.__name = "UTC"
        elif tz_sign < 0:
            self.__name = "-%s:%s" % (tz_hours, tz_minutes)
        else:
            self.__name = "+%s:%s" % (tz_hours, tz_minutes)

    def utcoffset(self, dt):
        return self.__offset

    def tzname(self, dt):
        return self.__name

    def dst(self, dt):
        return TZInfo.ZERO

